/*
 * The MIT License (MIT)
 * Copyright (c) 2018 Danijel Durakovic
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 */

///////////////////////////////////////////////////////////////////////////////
//
//  /mINI/ v0.9.7
//  An INI file reader and writer for the modern age.
//
///////////////////////////////////////////////////////////////////////////////
//
//  A tiny utility library for manipulating INI files with a straightforward
//  API and a minimal footprint. It conforms to the (somewhat) standard INI
//  format - sections and keys are case insensitive and all leading and
//  trailing whitespace is ignored. Comments are lines that begin with a
//  semicolon. Trailing comments are allowed on section lines.
//
//  Files are read on demand, upon which data is kept in memory and the file
//  is closed. This utility supports lazy writing, which only writes changes
//  and updates to a file and preserves custom formatting and comments. A lazy
//  write invoked by a write() call will read the output file, find what
//  changes have been made and update the file accordingly. If you only need to
//  generate files, use generate() instead. Section and key order is preserved
//  on read, write and insert.
//
///////////////////////////////////////////////////////////////////////////////
//
//  /* BASIC USAGE EXAMPLE: */
//
//  /* read from file */
//  mINI::INIFile file("myfile.ini");
//  mINI::INIStructure ini;
//  file.read(ini);
//
//  /* read value; gets a reference to actual value in the structure.
//     if key or section don't exist, a new empty value will be created */
//  std::string& value = ini["section"]["key"];
//
//  /* read value safely; gets a copy of value in the structure.
//     does not alter the structure */
//  std::string value = ini.get("section").get("key");
//
//  /* set or update values */
//  ini["section"]["key"] = "value";
//
//  /* set multiple values */
//  ini["section2"].set({
//      {"key1", "value1"},
//      {"key2", "value2"}
//  });
//
//  /* write updates back to file, preserving comments and formatting */
//  file.write(ini);
//
//  /* or generate a file (overwrites the original) */
//  file.generate(ini);
//
///////////////////////////////////////////////////////////////////////////////
//
//  Long live the INI file!!!
//
///////////////////////////////////////////////////////////////////////////////

#ifndef MINI_INI_H_
#define MINI_INI_H_

#include <sys/stat.h>

#include <algorithm>
#include <fstream>
#include <memory>
#include <sstream>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>

namespace mINI {
namespace INIStringUtil {
const std::string whitespaceDelimiters = " \t\n\r\f\v";
inline void trim(std::string &str) {
  str.erase(str.find_last_not_of(whitespaceDelimiters) + 1);
  str.erase(0, str.find_first_not_of(whitespaceDelimiters));
}
#ifndef MINI_CASE_SENSITIVE
inline void toLower(std::string &str) {
  std::transform(str.begin(), str.end(), str.begin(), ::tolower);
}
#endif
inline void replace(std::string &str, std::string const &a,
                    std::string const &b) {
  if (!a.empty()) {
    std::size_t pos = 0;
    while ((pos = str.find(a, pos)) != std::string::npos) {
      str.replace(pos, a.size(), b);
      pos += b.size();
    }
  }
}
#ifdef _WIN32
const std::string endl = "\r\n";
#else
const std::string endl = "\n";
#endif
}  // namespace INIStringUtil

template <typename T>
class INIMap {
 private:
  using T_DataIndexMap = std::unordered_map<std::string, std::size_t>;
  using T_DataItem = std::pair<std::string, T>;
  using T_DataContainer = std::vector<T_DataItem>;
  using T_MultiArgs = typename std::vector<std::pair<std::string, T>>;

  T_DataIndexMap dataIndexMap;
  T_DataContainer data;

  inline std::size_t setEmpty(std::string &key) {
    std::size_t index = data.size();
    dataIndexMap[key] = index;
    data.emplace_back(key, T());
    return index;
  }

 public:
  using const_iterator = typename T_DataContainer::const_iterator;

  INIMap() {}

  INIMap(INIMap const &other) {
    std::size_t data_size = other.data.size();
    for (std::size_t i = 0; i < data_size; ++i) {
      auto const &key = other.data[i].first;
      auto const &obj = other.data[i].second;
      data.emplace_back(key, obj);
    }
    dataIndexMap = T_DataIndexMap(other.dataIndexMap);
  }

  T &operator[](std::string key) {
    INIStringUtil::trim(key);
#ifndef MINI_CASE_SENSITIVE
    INIStringUtil::toLower(key);
#endif
    auto it = dataIndexMap.find(key);
    bool hasIt = (it != dataIndexMap.end());
    std::size_t index = (hasIt) ? it->second : setEmpty(key);
    return data[index].second;
  }
  T get(std::string key) const {
    INIStringUtil::trim(key);
#ifndef MINI_CASE_SENSITIVE
    INIStringUtil::toLower(key);
#endif
    auto it = dataIndexMap.find(key);
    if (it == dataIndexMap.end()) {
      return T();
    }
    return T(data[it->second].second);
  }
  bool has(std::string key) const {
    INIStringUtil::trim(key);
#ifndef MINI_CASE_SENSITIVE
    INIStringUtil::toLower(key);
#endif
    return (dataIndexMap.count(key) == 1);
  }
  void set(std::string key, T obj) {
    INIStringUtil::trim(key);
#ifndef MINI_CASE_SENSITIVE
    INIStringUtil::toLower(key);
#endif
    auto it = dataIndexMap.find(key);
    if (it != dataIndexMap.end()) {
      data[it->second].second = obj;
    } else {
      dataIndexMap[key] = data.size();
      data.emplace_back(key, obj);
    }
  }
  void set(T_MultiArgs const &multiArgs) {
    for (auto const &it : multiArgs) {
      auto const &key = it.first;
      auto const &obj = it.second;
      set(key, obj);
    }
  }
  bool remove(std::string key) {
    INIStringUtil::trim(key);
#ifndef MINI_CASE_SENSITIVE
    INIStringUtil::toLower(key);
#endif
    auto it = dataIndexMap.find(key);
    if (it != dataIndexMap.end()) {
      std::size_t index = it->second;
      data.erase(data.begin() + index);
      dataIndexMap.erase(it);
      for (auto &it2 : dataIndexMap) {
        auto &vi = it2.second;
        if (vi > index) {
          vi--;
        }
      }
      return true;
    }
    return false;
  }
  void clear() {
    data.clear();
    dataIndexMap.clear();
  }
  std::size_t size() const { return data.size(); }
  const_iterator begin() const { return data.begin(); }
  const_iterator end() const { return data.end(); }
};

using INIStructure = INIMap<INIMap<std::string>>;

namespace INIParser {
using T_ParseValues = std::pair<std::string, std::string>;

enum class PDataType : char {
  PDATA_NONE,
  PDATA_COMMENT,
  PDATA_SECTION,
  PDATA_KEYVALUE,
  PDATA_UNKNOWN
};

inline PDataType parseLine(std::string line, T_ParseValues &parseData) {
  parseData.first.clear();
  parseData.second.clear();
  INIStringUtil::trim(line);
  if (line.empty()) {
    return PDataType::PDATA_NONE;
  }
  char firstCharacter = line[0];
  if (firstCharacter == ';') {
    return PDataType::PDATA_COMMENT;
  }
  if (firstCharacter == '[') {
    auto commentAt = line.find_first_of(';');
    if (commentAt != std::string::npos) {
      line = line.substr(0, commentAt);
    }
    auto closingBracketAt = line.find_last_of(']');
    if (closingBracketAt != std::string::npos) {
      auto section = line.substr(1, closingBracketAt - 1);
      INIStringUtil::trim(section);
      parseData.first = section;
      return PDataType::PDATA_SECTION;
    }
  }
  auto lineNorm = line;
  INIStringUtil::replace(lineNorm, "\\=", "  ");
  auto equalsAt = lineNorm.find_first_of('=');
  if (equalsAt != std::string::npos) {
    auto key = line.substr(0, equalsAt);
    INIStringUtil::trim(key);
    INIStringUtil::replace(key, "\\=", "=");
    auto value = line.substr(equalsAt + 1);
    INIStringUtil::trim(value);
    parseData.first = key;
    parseData.second = value;
    return PDataType::PDATA_KEYVALUE;
  }
  return PDataType::PDATA_UNKNOWN;
}
}  // namespace INIParser

class INIReader {
 public:
  using T_LineData = std::vector<std::string>;
  using T_LineDataPtr = std::shared_ptr<T_LineData>;

 private:
  std::ifstream fileReadStream;
  T_LineDataPtr lineData;

  T_LineData readFile() {
    std::string fileContents;
    fileReadStream.seekg(0, std::ios::end);
    fileContents.resize(fileReadStream.tellg());
    fileReadStream.seekg(0, std::ios::beg);
    std::size_t fileSize = fileContents.size();
    fileReadStream.read(&fileContents[0], fileSize);
    fileReadStream.close();
    T_LineData output;
    if (fileSize == 0) {
      return output;
    }
    std::string buffer;
    buffer.reserve(50);
    for (std::size_t i = 0; i < fileSize; ++i) {
      char &c = fileContents[i];
      if (c == '\n') {
        output.emplace_back(buffer);
        buffer.clear();
        continue;
      }
      if (c != '\0' && c != '\r') {
        buffer += c;
      }
    }
    output.emplace_back(buffer);
    return output;
  }

 public:
  INIReader(std::string const &filename, bool keepLineData = false) {
    fileReadStream.open(filename, std::ios::in | std::ios::binary);
    if (keepLineData) {
      lineData = std::make_shared<T_LineData>();
    }
  }
  ~INIReader() {}

  bool operator>>(INIStructure &data) {
    if (!fileReadStream.is_open()) {
      return false;
    }
    T_LineData fileLines = readFile();
    std::string section;
    bool inSection = false;
    INIParser::T_ParseValues parseData;
    for (auto const &line : fileLines) {
      auto parseResult = INIParser::parseLine(line, parseData);
      if (parseResult == INIParser::PDataType::PDATA_SECTION) {
        inSection = true;
        data[section = parseData.first];
      } else if (inSection &&
                 parseResult == INIParser::PDataType::PDATA_KEYVALUE) {
        auto const &key = parseData.first;
        auto const &value = parseData.second;
        data[section][key] = value;
      }
      if (lineData && parseResult != INIParser::PDataType::PDATA_UNKNOWN) {
        if (parseResult == INIParser::PDataType::PDATA_KEYVALUE && !inSection) {
          continue;
        }
        lineData->emplace_back(line);
      }
    }
    return true;
  }
  T_LineDataPtr getLines() { return lineData; }
};

class INIGenerator {
 private:
  std::ofstream fileWriteStream;

 public:
  bool prettyPrint = false;

  INIGenerator(std::string const &filename) {
    fileWriteStream.open(filename, std::ios::out | std::ios::binary);
  }
  ~INIGenerator() {}

  bool operator<<(INIStructure const &data) {
    if (!fileWriteStream.is_open()) {
      return false;
    }
    if (!data.size()) {
      return true;
    }
    auto it = data.begin();
    for (;;) {
      auto const &section = it->first;
      auto const &collection = it->second;
      fileWriteStream << "[" << section << "]";
      if (collection.size()) {
        fileWriteStream << INIStringUtil::endl;
        auto it2 = collection.begin();
        for (;;) {
          auto key = it2->first;
          INIStringUtil::replace(key, "=", "\\=");
          auto value = it2->second;
          INIStringUtil::trim(value);
          fileWriteStream << key << ((prettyPrint) ? " = " : "=") << value;
          if (++it2 == collection.end()) {
            break;
          }
          fileWriteStream << INIStringUtil::endl;
        }
      }
      if (++it == data.end()) {
        break;
      }
      fileWriteStream << INIStringUtil::endl;
      if (prettyPrint) {
        fileWriteStream << INIStringUtil::endl;
      }
    }
    return true;
  }
};

class INIWriter {
 private:
  using T_LineData = std::vector<std::string>;
  using T_LineDataPtr = std::shared_ptr<T_LineData>;

  std::string filename;

  T_LineData getLazyOutput(T_LineDataPtr const &lineData, INIStructure &data,
                           INIStructure &original) {
    T_LineData output;
    INIParser::T_ParseValues parseData;
    std::string sectionCurrent;
    bool parsingSection = false;
    bool continueToNextSection = false;
    bool discardNextEmpty = false;
    bool writeNewKeys = false;
    std::size_t lastKeyLine = 0;
    for (auto line = lineData->begin(); line != lineData->end(); ++line) {
      if (!writeNewKeys) {
        auto parseResult = INIParser::parseLine(*line, parseData);
        if (parseResult == INIParser::PDataType::PDATA_SECTION) {
          if (parsingSection) {
            writeNewKeys = true;
            parsingSection = false;
            --line;
            continue;
          }
          sectionCurrent = parseData.first;
          if (data.has(sectionCurrent)) {
            parsingSection = true;
            continueToNextSection = false;
            discardNextEmpty = false;
            output.emplace_back(*line);
            lastKeyLine = output.size();
          } else {
            continueToNextSection = true;
            discardNextEmpty = true;
            continue;
          }
        } else if (parseResult == INIParser::PDataType::PDATA_KEYVALUE) {
          if (continueToNextSection) {
            continue;
          }
          if (data.has(sectionCurrent)) {
            auto &collection = data[sectionCurrent];
            auto const &key = parseData.first;
            auto const &value = parseData.second;
            if (collection.has(key)) {
              auto outputValue = collection[key];
              if (value == outputValue) {
                output.emplace_back(*line);
              } else {
                INIStringUtil::trim(outputValue);
                auto lineNorm = *line;
                INIStringUtil::replace(lineNorm, "\\=", "  ");
                auto equalsAt = lineNorm.find_first_of('=');
                auto valueAt = lineNorm.find_first_not_of(
                  INIStringUtil::whitespaceDelimiters, equalsAt + 1);
                std::string outputLine = line->substr(0, valueAt);
                if (prettyPrint && equalsAt + 1 == valueAt) {
                  outputLine += " ";
                }
                outputLine += outputValue;
                output.emplace_back(outputLine);
              }
              lastKeyLine = output.size();
            }
          }
        } else {
          if (discardNextEmpty && line->empty()) {
            discardNextEmpty = false;
          } else if (parseResult != INIParser::PDataType::PDATA_UNKNOWN) {
            output.emplace_back(*line);
          }
        }
      }
      if (writeNewKeys || std::next(line) == lineData->end()) {
        T_LineData linesToAdd;
        if (data.has(sectionCurrent) && original.has(sectionCurrent)) {
          auto const &collection = data[sectionCurrent];
          auto const &collectionOriginal = original[sectionCurrent];
          for (auto const &it : collection) {
            auto key = it.first;
            if (collectionOriginal.has(key)) {
              continue;
            }
            auto value = it.second;
            INIStringUtil::replace(key, "=", "\\=");
            INIStringUtil::trim(value);
            linesToAdd.emplace_back(key + ((prettyPrint) ? " = " : "=") +
                                    value);
          }
        }
        if (!linesToAdd.empty()) {
          output.insert(output.begin() + lastKeyLine, linesToAdd.begin(),
                        linesToAdd.end());
        }
        if (writeNewKeys) {
          writeNewKeys = false;
          --line;
        }
      }
    }
    for (auto const &it : data) {
      auto const &section = it.first;
      if (original.has(section)) {
        continue;
      }
      if (prettyPrint && output.size() > 0 && !output.back().empty()) {
        output.emplace_back();
      }
      output.emplace_back("[" + section + "]");
      auto const &collection = it.second;
      for (auto const &it2 : collection) {
        auto key = it2.first;
        auto value = it2.second;
        INIStringUtil::replace(key, "=", "\\=");
        INIStringUtil::trim(value);
        output.emplace_back(key + ((prettyPrint) ? " = " : "=") + value);
      }
    }
    return output;
  }

 public:
  bool prettyPrint = false;

  INIWriter(std::string const &file_name) : filename(file_name) {}
  ~INIWriter() {}

  bool operator<<(INIStructure &data) {
    struct stat buf;
    bool fileExists = (stat(filename.c_str(), &buf) == 0);
    if (!fileExists) {
      INIGenerator generator(filename);
      generator.prettyPrint = prettyPrint;
      return generator << data;
    }
    INIStructure originalData;
    T_LineDataPtr lineData;
    bool readSuccess = false;
    {
      INIReader reader(filename, true);
      if ((readSuccess = reader >> originalData)) {
        lineData = reader.getLines();
      }
    }
    if (!readSuccess) {
      return false;
    }
    T_LineData output = getLazyOutput(lineData, data, originalData);
    std::ofstream fileWriteStream(filename, std::ios::out | std::ios::binary);
    if (fileWriteStream.is_open()) {
      if (output.size()) {
        auto line = output.begin();
        for (;;) {
          fileWriteStream << *line;
          if (++line == output.end()) {
            break;
          }
          fileWriteStream << INIStringUtil::endl;
        }
      }
      return true;
    }
    return false;
  }
};

class INIFile {
 private:
  std::string filename;

 public:
  INIFile(std::string const &file_name) : filename(file_name) {}

  ~INIFile() {}

  bool read(INIStructure &data) const {
    if (data.size()) {
      data.clear();
    }
    if (filename.empty()) {
      return false;
    }
    INIReader reader(filename);
    return reader >> data;
  }
  bool generate(INIStructure const &data, bool pretty = false) const {
    if (filename.empty()) {
      return false;
    }
    INIGenerator generator(filename);
    generator.prettyPrint = pretty;
    return generator << data;
  }
  bool write(INIStructure &data, bool pretty = false) const {
    if (filename.empty()) {
      return false;
    }
    INIWriter writer(filename);
    writer.prettyPrint = pretty;
    return writer << data;
  }
};
}  // namespace mINI

#endif  // MINI_INI_H_
